#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/select.h>
#include <unistd.h>

#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>


//TCP服务器使用select实现多路复用



//定义一个结构体来保存文件描述符集和当前关注的最大文件描述
typedef struct Fd_set
{
  fd_set fds;
  int max_fd;
}Fd_set;

//初始化文件描述符集
void Init_set(Fd_set * Fds)
{
  if(Fds == NULL)
    return;
  Fds->max_fd = -1;
  FD_ZERO(&Fds->fds);
}
//对文件描述符集进行设置新的文件描述符
void Set_fds(int fd,Fd_set * Fds)
{
  if(Fds == NULL)
    return;
  //更新 max_fd
  if(Fds->max_fd < fd)
  {
    Fds->max_fd = fd;
  }
  //设置fds
  FD_SET(fd,&Fds->fds);
}
//从文件描述符集中删除一个文件描述符
void Delete_fds(int fd,Fd_set * Fds)
{
  if(Fds == NULL)
    return;
  if(fd > Fds->max_fd)
    return;
  if(Fds->max_fd > fd)
  {//如果最大的文件描述符大于要删除的文件描述符
    //可以直接进行删除
    FD_CLR(fd,&Fds->fds);
  }
  else
  {//如果最大的文件描述符等于要删除的文件描述符
    //此时就要重新设最大文件描述符
    int i = 0;
    int max_fd = -1;
    FD_CLR(fd,&Fds->fds);
    //此处采用从最大的开始找的方法，虽然时间复杂度都为O(N)
    for(i = Fds->max_fd;i >= 0; --i)
    {
        if(FD_ISSET(i,&Fds->fds))
        {
          max_fd = i ;
        }
    }
    Fds->max_fd = max_fd;
  }
}

//获取listen_socket
int server_start(char * IP,short Port)
{
  //创建socket
  int listen_socket = socket(AF_INET,SOCK_STREAM,0);
  if(listen_socket < 0)
  {
    perror("socket");
    return -1;
  }
  //定义sockaddr_in 结构
  sockaddr_in addr;
  socklen_t addr_len = sizeof(addr);
  addr.sin_family = AF_INET;//IPV4
  addr.sin_addr.s_addr = inet_addr(IP);
  addr.sin_port = htons(Port);

  //绑定IP和端口号
  int ret = bind(listen_socket,(sockaddr *)&addr,addr_len);
  if(ret < 0)
  {
    perror("bind");
    return -1;
  }
  //监听socket使其处于可以被建立连接状态
  ret = listen(listen_socket,5);
  if(ret < 0)
  {
    return -1;
  }
  //将listen_socket返回
  return listen_socket;
}

int process_connection(int new_socket,sockaddr_in *peer)
{
  //这里处理连接就只读一次，因为我们将所有的等到都交给了select 
  //这里只进行读操作和响应
  char buf[1024] = {0};
  ssize_t read_size = read(new_socket,buf,sizeof(buf) - 1);
  if(read_size < 0)
  {
    perror("read");
    return -1;
  }
  if(read_size == 0)
  {
    printf("read done\n");
    return 0;
  }
  buf[read_size] = '\0';
  printf("perr [%s:%d]  say : %s\n",inet_ntoa(peer->sin_addr),ntohs(peer->sin_port),buf);

  //回显服务，收到什么内容，就回复什么内容
  write(new_socket,buf,strlen(buf));
  return 1;
}

int main(int argc ,char * argv[])
{
  //1.判断命令行参数正确性
  if(argc != 3)
  {
    printf("Usage [./server] [IP]  [Port]\n");
    return 1;
  }

  int listen_socket =server_start( argv[1],atoi(argv[2]));
  if(listen_socket < 0)
  {
    printf("start faile\n");
    return -1; 
  }
  printf("start ok\n");

  //定义文件描述符集并进行初始化和设置
  Fd_set in_put;
  Init_set(&in_put);
  Set_fds(listen_socket,&in_put);

  //因为这里第二个参数既是输入行参数，又是输出型参数
  //为了防止将第二次需要再次等待的文件描述符集破坏掉，需要定义一个输出参数用来保存输出文件描述符集
  Fd_set out_put = in_put;

  //进行事件循环
  while(1)
  {
    sockaddr_in peer;
    socklen_t peer_len = sizeof(peer);
    in_put = out_put;
    int ret = select(in_put.max_fd + 1,&in_put.fds,NULL,NULL,NULL);
    if(ret < 0)
    {
      printf("select filed\n");
      continue;
    }
    if(ret == 0)
    {
      printf("time put\n");
      continue;
    }
    //判断listen_sock是否就绪，若就绪，就可以进行accpet()
    if(FD_ISSET(listen_socket,&in_put.fds))
    {
      int new_socket = accept(listen_socket,(sockaddr *)&peer,&peer_len);
      if(new_socket < 0)
      {
        perror("accept");
        continue;
      }
      else
      {//此处建立连接成功后，应该将 new_socket设置在out_put中保存下来中
        Set_fds(new_socket,&out_put);
        printf("client %d connect!\n",new_socket);
      }
    }
    else
    {//处理已经就绪的new_sock
      int i = 0;
      for(i = 0; i < in_put.max_fd + 1; ++i)
      {
        if(FD_ISSET(i , &in_put.fds))
        {//如果这个文件描述符已经就绪，就进行处理本次连接
          //将这一位文件描述符进行设置
          Set_fds(i , &out_put);
          int ret = process_connection(i,&peer);
          if(ret <= 0)
          {//说明本次连接已经结束
            //需要将out_put里面的这个文件描述清理掉
            Delete_fds(i,&out_put);
            close(i);
            printf("%d closed \n",i);
          }
        }
        else
        {
          //如果还没有就绪
          //就检测其他的文件描述符
          continue;
        }
      }
    }
  }
}

//其实这里还存在一个问题，上面我们处理listen_socket和new_socket的处理是is-else结构的
//但是listen_socket和new_socket是很有可能同时就绪的
//但是这里也影响，因为这次没有处理，循环会继续执行，会在下一次循环处理
//我们将这种处理方式称为水平触发
